<?php
/**
 * Created by PhpStorm.
 * User: DELL-PC
 * Date: 2018/5/19
 * Time: 13:06
 */

namespace J\Util;


use J\Exception;
use J\Message;

/**
 * 路由服务
 * @package J\Web
 */
class Router
{
    const   REGEX = 'regex';
    const   EXT   = 'ext';

    const   CFG_KEY_ENTITY   = 'entities';
    const   CFG_KEY_BUILDER  = 'builders';
    const   CFG_KEY_CALLBACK = 'callbacks';
    const   CFG_KEY_RULE     = 'rules';
    const   CFG_KEY_INTENT   = 'intents';

    /**
     * 路由参数: 路由实体定义  :name
     * @var array
     */
    protected $entities    = array();

    /**
     * 路由参数: 路由规则定义
     * @var array
     */
    protected $rules       = array();

    /**
     *  自定义路由参数解析方法
     *  function(string 原始值, array &参数对象, string 键值 = null)
     * @var array
     */
    protected $callbacks    = array();

    /**
     * @var array 自定义意图解析
     */
    protected $intents     = array();

    /**
     * 自定义路由实体匹配方法
     * function(string 实体定义, string 匹配值)
     * @var array
     */
    protected $builders     = array();

    /**
     * 内部使用: 完成正则化的路由规则
     *
     * @var string[]
     */
    protected $parsed_rules = array();

    /**
     * 内部使用: 路由规则所需的路由实体集合
     *
     * @var array
     */
    protected $rules_require = array();

    /**
     * 配置路由器参数
     * @param array $config 参数
     */
    public function setConfig(array $config)
    {
        $this->parsed_rules = array();
        // 解析路由实体
        isset($config[self::CFG_KEY_ENTITY]) and $this->prepareEntities($config[self::CFG_KEY_ENTITY]);
        // 路由规则懒解析，自定义内容直接赋值
        foreach([self::CFG_KEY_RULE, self::CFG_KEY_CALLBACK, self::CFG_KEY_BUILDER, self::CFG_KEY_INTENT] as $property){
            $this->set($property, $config);
        }
    }

    /**
     * 对PATH进行路由计算
     *
     * @param string $command
     * @return array 匹配到的参数
     * @throws Exception
     */
    public function dispatch($command)
    {
        $command = trim($command, '/');

        // 自定义解析
        $command = array_isset($this->intents, $command, $command);

        if(is_array($command)) return $command;

        // 自动获取后缀
        $ext  = (strrpos($command,'.') ) ? substr($command,strrpos($command,'.')) : null;

        foreach ($this->rules as $regex => $param) {
            if (!$this->matchWhere($param)) continue;

            $values = $this->matchRegex($command, $regex, $param);
            if ($values === false)  continue;

            // 记录路由规则和文件后缀
            $values[self::REGEX] = $regex;
            $values[self::EXT]   = $ext;
            return $values;
        }

        var_dump($this->entities);

        throw new Exception(Message::INTENT_NOT_FIND, ['command' => $command]);
    }

    /**
     * 组装URL
     * @param array $values 参数
     * @param string $regex 路由规则
     * @return bool|string  url
     */
    public function getUrl($values, $regex = null)
    {
        // regex 是Route关键字，为防止误操作，保证程序流畅，禁止输出该变量
        unset($values[self::REGEX]);

        // 处理后缀
        $ext = ( isset($values[self::EXT])
            and isset($this->entities[':'.self::EXT])
            and $this->matchValue(self::EXT,$values[self::EXT])
        ) ? $values[self::EXT] : '';
        unset($values[self::EXT]);

        // 指定了路由规则，直接验证
        if($regex !== null) return $this->createUrl($values, $regex, $ext);
        // 轮询路由表，匹配验证
        foreach ($this->rules as $regex => $param) {
            $url = $this->makeUrl($regex, $param, $values, $ext);
            if($url === false) continue;
            return $url;
        }
        return false;
    }

    protected function set($property, $config)
    {
        $this->$property = (isset($config[$property]) and is_array($config[$property])) ? $config[$property] : array();
    }

    /**
     * 编译并初始化路由实体
     * @param array $entities
     */
    protected function prepareEntities($entities)
    {
        foreach ((array)$entities as $name => $regex) {
            $this->entities[':' . $name] = '(?<' . $name . '>' . $regex . ')';
        }
    }

    /**
     * 将来扩展
     * @param array $param
     * @return boolean
     */
    protected function matchWhere($param)
    {
        // 请求方法限制
        if (isset($param['on']) and strtolower(http_method()) != strtolower($param['on'])) return false;
        // 默认返回
        return true;
    }

    /**
     * 路由匹配具体操作
     * @param string $query
     * @param string $regex
     * @param array $param
     * @return boolean|array
     */
    protected function matchRegex($query, $regex, $param = array())
    {
        //转换为正则路由
        if (!preg_match("@^/.*/[isUx]*$@i", $regex)) {
            $regex = $this->getBaseRegex($regex);
        }

        //路由匹配
        if (preg_match($regex, $query, $match)) {
            return $this->parseMatch($match, $param);
        }
        return false;
    }

    /**
     * 正则化路由规则
     * @param string $regex
     * @return string
     */
    protected function getBaseRegex($regex)
    {
        if (!isset($this->parsed_rules[$regex])) {
            $this->parsed_rules[$regex] = "@^" . \strtr($regex, $this->entities) . "@i";
        }
        return $this->parsed_rules[$regex];
    }

    /**
     * 返回路由匹配后获取的参数
     * @param array $match
     * @param array $param
     * @return array
     */
    protected function parseMatch($match, $param)
    {
        foreach ($match as $key => $value) {
            if (is_numeric($key) and isset($param[$key - 1])) {
                $param[$param[$key - 1]] = $value;
                unset($param[$key - 1]);
            } elseif (is_string($key) and !isset($param[$key])) {
                $this->prepareValue($key,$value,$param);
            }
        }
        return $param;
    }

    /**
     * 扩展接口（自定义实体处理接口）
     * @param string $key
     * @param string $value
     * @param array $param
     */
    protected function prepareValue($key, $value, &$param)
    {
        if(isset($this->callbacks[$key]) and  is_callable($this->callbacks[$key])){
            $function = $this->callbacks[$key];
            $function($value, $param, $key);
        }else{
            $param[$key] = $value;
        }
    }

    protected function createUrl($values, $regex, $ext)
    {
        if(!isset($this->rules[$regex])) return false;
        return $this->makeUrl($regex,$this->rules[$regex],$values, $ext);
    }

    protected function makeUrl($regex,$param,$values, $ext)
    {
        $replace = array();

        foreach ($this->getRegexRequire($regex) as $key) {
            $realKey = substr($key,1);
            if ( // 必需参数不匹配
                !isset($values[$realKey]) or
                !$this->matchValue($realKey, $values[$realKey])
            ) {
                return false;
            }
            $replace[$key] = $values[$realKey];
            unset($values[$realKey]);
        }

        if(empty($param)) $param = [];

        foreach ($param as $k => $v) {
            // 参数不匹配
            if (isset($values[$k]) and $values[$k] !== $v
            ) {
                return false;
            }
            unset($values[$k]);
        }
        $queryString = (empty($values))?'':('?'.http_build_query($values));
        return '/'.\strtr($regex, $replace).$ext.$queryString;
    }

    protected function matchValue($key,$value)
    {
        if(isset($this->builders[$key])){
            $function = $this->builders[$key];
            return is_callable($function) ? $function($this->entities[':'.$key], $value) : true;
        }else{
            return preg_match('@^' . $this->entities[':'.$key] . '$@i', $value);
        }
    }

    protected function getRegexRequire($regex)
    {
        if (!isset($this->rules_require[$regex])) {
            preg_match_all('/:([a-zA-z\d]+)/i', $regex, $matches);
            // 求交集
            $this->rules_require[$regex] = array_intersect($matches[0], array_keys($this->entities));
        }
        return $this->rules_require[$regex];
    }
}